use std::str::FromStr;
use std::sync::Arc;

use aspect_proto::auth::AspAuthConfig;
use ecksport_rpc::client::RpcClient;
use ecksport_rpc::errors::ClientError;
use thiserror::Error;
use tokio::sync::RwLock;
use tracing::*;

use aspect_codec::ecksport::AsAspCodec;
use aspect_core::addrfmt;
use aspect_core::message_queue::MessageQueueId;
use aspect_db_common::state_prop::StatePropDatastore;
use aspect_ident::{DeviceIdent, DirPackage, IdentSigner};
use aspect_proto::identity::AnnResponse;
use aspect_proto::msg_queue::{MqListRequest, QueueViewState};
use aspect_proto::user::UserCreds;
use aspect_proto::{AspClientError as ProtoError, HomeserverClient};

use crate::credentials::HomeserverCreds;
use crate::signer_manager::SignerManager;

/// Homeserver credentials.
pub const DSK_HOMESERVER_CREDS: &str = "homeserver_creds";

#[derive(Debug, Error)]
pub enum Error {
    #[error("homeserver creds not set")]
    NoHomeserverCreds,

    #[error("missing homeserver connection")]
    NoHomeserverConn,

    #[error("addrfmt: {0}")]
    Addr(#[from] addrfmt::Error),

    #[error("proto: {0}")]
    Proto(#[from] ProtoError),

    #[error("ecksrpc client: {0}")]
    EcksClient(#[from] ClientError),

    #[error("ecksnet: {0}")]
    EcksNet(#[from] ecksport_net::errors::Error),

    #[error("datastore: {0}")]
    Datastore(#[from] aspect_db_common::errors::Error),
}

/// Used to indicate how we decide to authenticate with a server.
#[derive(Clone)]
pub enum PeerAuthConfig {
    Anon,
    Ident(Arc<IdentSigner>),
}

impl PeerAuthConfig {
    /// Constructs an [``AspAuthConfig``] from the auth config.
    pub fn to_ecks_auth_config(&self) -> AspAuthConfig {
        match self {
            Self::Anon => AspAuthConfig::new_unauth(),
            Self::Ident(signer) => AspAuthConfig::new_auth(signer.clone()),
        }
    }
}

/// Manages a client's peers, in the context-dependent forms they might take.
pub struct PeerManager {
    stateprop_ds: Arc<StatePropDatastore>,
    creds: RwLock<Option<HomeserverCreds>>,
    homeserver_client: RwLock<Option<Arc<HomeserverClient>>>,
    signer_man: Arc<RwLock<SignerManager>>,
    peer_auth_config: PeerAuthConfig,
}

impl PeerManager {
    pub async fn new(
        stateprop_ds: Arc<StatePropDatastore>,
        signer_man: Arc<RwLock<SignerManager>>,
        peer_auth_config: PeerAuthConfig,
    ) -> Result<Self, Error> {
        let creds = stateprop_ds
            .get_prop::<HomeserverCreds>(DSK_HOMESERVER_CREDS)
            .await?;

        Ok(Self {
            stateprop_ds,
            creds: RwLock::new(creds),
            homeserver_client: RwLock::new(None),
            signer_man,
            peer_auth_config,
        })
    }

    pub fn has_creds_sync(&self) -> bool {
        let creds = self.creds.blocking_read();
        creds.is_some()
    }

    pub async fn has_creds_async(&self) -> bool {
        let creds = self.creds.read().await;
        creds.is_some()
    }

    pub async fn get_homeserver_client(&self) -> Option<Arc<HomeserverClient>> {
        let hsc = self.homeserver_client.read().await;
        hsc.as_ref().cloned()
    }

    pub async fn get_homeserver_client_ok(&self) -> Result<Arc<HomeserverClient>, Error> {
        self.get_homeserver_client()
            .await
            .ok_or(Error::NoHomeserverConn)
    }

    async fn get_signer_ident(&self) -> DeviceIdent {
        let sm = self.signer_man.read().await;
        *sm.current_update_pkg().ident()
    }

    /// Sets the homeserver creds, both in memory and in the datastore.
    pub async fn install_creds(&self, creds: HomeserverCreds) -> Result<(), Error> {
        let mut c = self.creds.write().await;
        self.stateprop_ds
            .set_prop(DSK_HOMESERVER_CREDS, &creds)
            .await?;
        *c = Some(creds);
        Ok(())
    }

    pub async fn get_creds(&self) -> Option<HomeserverCreds> {
        let creds = self.creds.read().await;
        creds.clone()
    }

    pub async fn get_creds_ok(&self) -> Result<HomeserverCreds, Error> {
        self.get_creds().await.ok_or(Error::NoHomeserverCreds)
    }

    /// Creates an instance of an auth config to use based on the peer auth
    /// config.
    pub fn create_ecks_auth_config(&self) -> AspAuthConfig {
        self.peer_auth_config.to_ecks_auth_config()
    }

    /// Connects to a homeserver and registers an account on it.
    pub async fn connect_and_register(
        &self,
        homeserver: String,
        username: String,
        password: String,
    ) -> Result<(), Error> {
        // Connect and register the account.
        info!(%homeserver, "connecting to homeserver...");
        let addr = addrfmt::Address::from_str(&homeserver)?;

        let acfg = self.create_ecks_auth_config();
        let rpc_client = connect_to_server(addr, acfg).await?;
        let client = HomeserverClient::from_client(rpc_client);
        let client = Arc::new(client);
        client
            .user_register(username.clone(), password.clone(), Vec::new())
            .await?;
        info!(%homeserver, %username, "registered at homeserver!");

        // Now we can store the credentials.
        let creds = HomeserverCreds::new(homeserver, username.clone(), password.clone());
        self.install_creds(creds).await?;
        let mut hsc = self.homeserver_client.write().await;
        *hsc = Some(client.clone());

        // Now associate this device with the server.
        let dirpkg = {
            let sman = self.signer_man.read().await;
            sman.current_dirpkg().clone()
        };

        let creds = make_user_creds(self.get_creds_ok().await?);
        client
            .user_add_device(AsAspCodec::from(creds), AsAspCodec::from(dirpkg))
            .await?;

        Ok(())
    }

    /// Connects to a homeserver and logs into an account.  Registers device if
    /// not already registered.
    pub async fn connect_and_login(
        &self,
        homeserver: String,
        username: String,
        password: String,
    ) -> Result<(), Error> {
        // Connect and register the account.
        info!(%homeserver, "connecting to homeserver...");
        let addr = addrfmt::Address::from_str(&homeserver)?;

        let acfg = self.create_ecks_auth_config();
        let rpc_client = connect_to_server(addr, acfg).await?;
        let client = HomeserverClient::from_client(rpc_client);
        let client = Arc::new(client);

        // Now we can store the credentials.
        let creds = HomeserverCreds::new(homeserver, username.clone(), password.clone());
        self.install_creds(creds).await?;

        // If we've already registered our device to the server, do that.
        // TODO also ingest all these devices and see if there's any new ones
        let cur_ident = self.get_signer_ident().await;
        let devices = client.get_user_devices(username.clone()).await?;
        let is_registered = devices
            .iter()
            .any(|pkg| *pkg.last_update().ident() == cur_ident);
        if !is_registered {
            let sm = self.signer_man.read().await;
            client
                .register_device(username, password, sm.current_dirpkg().clone())
                .await?;
        }

        let mut hsc = self.homeserver_client.write().await;
        *hsc = Some(client);
        Ok(())
    }

    /// Reconnects to the server using the stored creds.
    pub async fn reconnect_homeserver(&self) -> Result<(), Error> {
        // TODO make a connection pool thing

        let creds = self.creds.read().await;
        let HomeserverCreds { homeserver, .. } =
            creds.as_ref().ok_or_else(|| Error::NoHomeserverCreds)?;

        info!(%homeserver, "connecting to homeserver...");
        let addr = addrfmt::Address::from_str(&homeserver)?;
        let acfg = self.create_ecks_auth_config();
        let rpc_client = connect_to_server(addr, acfg).await?;
        let client = HomeserverClient::from_client(rpc_client);
        let mut hsc = self.homeserver_client.write().await;
        *hsc = Some(Arc::new(client));

        Ok(())
    }

    /// Broadcasts dirpkg to all peers.
    pub async fn broadcast_dirpkg(&self, dirpkg: DirPackage) -> Result<bool, Error> {
        // TODO this just does it to the homeserver, for now
        let hsc = self.get_homeserver_client_ok().await?;
        let res = hsc.ident_announce(AsAspCodec::from(dirpkg)).await?;

        match res.inner() {
            AnnResponse::Accepted(_) => Ok(true),
            AnnResponse::Rejected(msg) => {
                warn!(%msg, "dirpkg announcement rejected by peer");
                Ok(false)
            }
        }
    }

    /// Queries the list of device queues we have access to on this homeserver.
    pub async fn list_device_queues(&self) -> Result<Vec<QueueViewState>, Error> {
        // TODO this just does it to the homeserver, for now
        let hsc = self.get_homeserver_client_ok().await?;
        // TODO temporary, cleanup
        let ident = self.get_signer_ident().await;

        let req = MqListRequest {
            creds: make_user_creds(self.get_creds_ok().await?),
            device_ident: ident,
        };

        let mqs = hsc.mq_list(AsAspCodec::from(req)).await?;
        Ok(mqs.into_inner().queues)
    }

    /// Queries the messages from the server notifications queue and interprets
    /// them as simple strings lossily.
    pub async fn get_server_notifs(&self) -> Result<Vec<String>, Error> {
        let hsc = self.get_homeserver_client_ok().await?;
        // TODO temporary, cleanup
        let creds = self.get_creds_ok().await?;
        let ident = self.get_signer_ident().await;

        let mqid =
            MessageQueueId::Server(aspect_db_common::constants::DMQID_SERVER_NOTIFICATIONS.to_string());

        let mut msgs = Vec::new();

        let stride = 100;
        let mut start = 0;

        loop {
            let ents = hsc
                .query_queue_messages(
                    creds.username.clone(),
                    creds.password.clone(),
                    ident,
                    mqid.clone(),
                    start,
                    start + stride,
                )
                .await?;

            for e in ents.entries {
                msgs.push(String::from_utf8_lossy(&e.data).to_string());
            }

            if ents.cur_queue_pos < start + stride {
                break;
            }

            start += stride;
        }

        Ok(msgs)
    }

    /// Sends a raw, unformatted D2D message to the specified device.  This is
    /// not idempotent, multiple identical calls may result in multiple messages
    /// being sent.
    pub async fn send_raw_d2d_msg(
        &self,
        target_id: &DeviceIdent,
        payload: &[u8],
    ) -> Result<(), Error> {
        // TODO decide if we can send this more directly than handing it off to our homeserver to do

        let hsc = self.get_homeserver_client_ok().await?;
        // TODO temporary, cleanup
        let creds = self.get_creds_ok().await?;
        let ident = self.get_signer_ident().await;

        debug!(%target_id, "PeerManager::send_raw_d2d_message");
        hsc.send_d2d_msg(
            creds.username,
            creds.password,
            ident,
            *target_id,
            payload.to_vec(),
        )
        .await?;

        Ok(())
    }
}

async fn connect_to_server(
    addr: addrfmt::Address,
    auth: AspAuthConfig,
) -> Result<RpcClient, Error> {
    match addr {
        addrfmt::Address::Sock(addr) => {
            let conn = ecksport_net::builder::ClientBuilder::new(aspect_proto::protocol::PROTO_V0)
                .connect_tcp_authed(addr, auth)
                .await?;

            Ok(ecksport_rpc::client::RpcClient::new(conn))
        }
    }
}

fn make_user_creds(hs_creds: HomeserverCreds) -> UserCreds {
    UserCreds {
        username: hs_creds.username,
        password: hs_creds.password,
    }
}
